Slovenčina

Komplexný sprievodca TypeScript generikami, pokrývajúci syntax, výhody a osvedčené postupy pre prácu s komplexnými dátovými typmi v globálnom softvéri.

TypeScript Generiká: Zvládnutie komplexných dátových typov pre robustné aplikácie

TypeScript, nadmnožina JavaScriptu, umožňuje vývojárom písať robustnejší a udržateľnejší kód pomocou statického typovania. Medzi jeho najvýkonnejšie funkcie patria generiká, ktoré vám umožňujú písať kód, ktorý môže pracovať s rôznymi dátovými typmi pri zachovaní typovej bezpečnosti. Tento sprievodca poskytuje komplexný prieskum TypeScript generík so zameraním na ich aplikáciu na komplexné dátové typy v kontexte globálneho vývoja softvéru.

Čo sú to generiká?

Generiká poskytujú spôsob, ako písať opakovane použiteľný kód, ktorý môže pracovať s rôznymi typmi. Namiesto písania samostatných funkcií alebo tried pre každý typ, ktorý chcete podporovať, môžete napísať jednu funkciu alebo triedu, ktorá používa typové parametre. Tieto typové parametre sú zástupné symboly pre skutočné typy, ktoré sa použijú pri volaní alebo inštancovaní funkcie alebo triedy. Je to obzvlášť užitočné pri práci s komplexnými dátovými štruktúrami, kde sa typ dát v týchto štruktúrach môže líšiť.

Výhody používania generík

Základná syntax generík

Základná syntax generík zahŕňa použitie lomených zátvoriek (< >) na deklarovanie typových parametrov. Tieto typové parametre sa zvyčajne nazývajú T, K, V atď., ale môžete použiť akýkoľvek platný identifikátor. Tu je jednoduchý príklad generickej funkcie:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Výstup: hello
console.log(myNumber); // Výstup: 123
console.log(myBoolean); // Výstup: true

V tomto príklade <T> deklaruje typový parameter s názvom T. Funkcia identity prijíma argument typu T a vracia hodnotu typu T. Pri volaní funkcie môžete explicitne špecifikovať typový parameter (napr. identity<string>) alebo nechať TypeScript, aby ho odvodil na základe typu argumentu.

Práca s komplexnými dátovými typmi

Generiká sa stávajú obzvlášť cennými pri práci s komplexnými dátovými typmi, ako sú polia, objekty a rozhrania. Pozrime sa na niekoľko bežných scenárov:

Generické polia

Môžete použiť generiká na vytváranie funkcií alebo tried, ktoré pracujú s poľami rôznych typov:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Výstup: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Výstup: apple, banana, cherry

Tu funkcia arrayToString prijíma pole typu T[] a vracia reťazcovú reprezentáciu poľa. Táto funkcia funguje s poľami akéhokoľvek typu, čo ju robí vysoko opakovane použiteľnou.

Generické objekty

Generiká sa môžu tiež použiť na definovanie funkcií alebo tried, ktoré pracujú s objektmi rôznych tvarov:


interface Person {
  name: string;
  age: number;
  country: string; // Pridaná krajina pre globálny kontext
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // Pridaná mena pre globálny kontext
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Výstup: Name: Alice
displayInfo(product); // Výstup: Name: Laptop

V tomto príklade funkcia displayInfo prijíma objekt typu T, ktorý musí mať vlastnosť name typu string. Klauzula extends { name: string } je obmedzenie (constraint), ktoré špecifikuje minimálne požiadavky na typový parameter T. Tým sa zabezpečí, že funkcia môže bezpečne pristupovať k vlastnosti name.

Pokročilé použitie generík

TypeScript generiká ponúkajú pokročilejšie funkcie, ktoré vám umožňujú vytvárať ešte flexibilnejší a výkonnejší kód. Pozrime sa na niektoré z týchto funkcií:

Viacnásobné typové parametre

Môžete definovať funkcie alebo triedy s viacerými typovými parametrami:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Výstup: Bob
console.log(merged.age); // Výstup: 42

Funkcia merge prijíma dva objekty typov T a U a vracia nový objekt, ktorý obsahuje vlastnosti oboch objektov. Je to výkonný spôsob, ako kombinovať dáta z rôznych zdrojov.

Generické obmedzenia

Ako už bolo ukázané, obmedzenia vám umožňujú obmedziť typy, ktoré sa môžu použiť s generickým typovým parametrom. Tým sa zabezpečí, že generický kód môže bezpečne pracovať so špecifikovanými typmi.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Výstup: 3
loggingIdentity("hello"); // Výstup: 5
// loggingIdentity(123); // Chyba: Argument typu 'number' nie je priraditeľný k parametru typu 'Lengthwise'.

Funkcia loggingIdentity prijíma argument typu T, ktorý musí mať vlastnosť length typu number. Tým sa zabezpečí, že funkcia môže bezpečne pristupovať k vlastnosti length.

Generické triedy

Generiká sa môžu použiť aj s triedami:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Výstup: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Výstup: [ 2 ]

Trieda DataStorage môže ukladať dáta akéhokoľvek typu T. To vám umožňuje vytvárať opakovane použiteľné dátové štruktúry, ktoré sú typovo bezpečné.

Generické rozhrania

Generické rozhrania sú užitočné na definovanie kontraktov, ktoré môžu pracovať s rôznymi typmi. Napríklad:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "User not found" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

Rozhranie Result definuje generickú štruktúru na reprezentáciu výsledku operácie. Môže obsahovať buď dáta typu T, alebo chybu typu E. Toto je bežný vzor na spracovanie asynchrónnych operácií alebo operácií, ktoré môžu zlyhať.

Pomocné typy a generiká

TypeScript poskytuje niekoľko vstavaných pomocných typov (utility types), ktoré dobre fungujú s generikami. Tieto pomocné typy vám môžu pomôcť transformovať a manipulovať s typmi výkonnými spôsobmi.

Partial<T>

Partial<T> robí všetky vlastnosti typu T voliteľnými:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Platné

Readonly<T>

Readonly<T> robí všetky vlastnosti typu T iba na čítanie (readonly):


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Chyba: Nemožno priradiť k 'age', pretože je to vlastnosť iba na čítanie.

Pick<T, K>

Pick<T, K> vyberie sadu vlastností K z typu T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K> odstráni sadu vlastností K z typu T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T> vytvára typ s kľúčmi K a hodnotami typu T:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Rozšírený zoznam pre globálny kontext
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Rozšírený zoznam pre globálny kontext

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

Mapované typy

Mapované typy vám umožňujú transformovať existujúce typy iteráciou cez ich vlastnosti. Je to výkonný spôsob, ako vytvárať nové typy na základe existujúcich. Napríklad, môžete vytvoriť typ, ktorý robí všetky vlastnosti iného typu iba na čítanie:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Chyba: Nemožno priradiť k 'age', pretože je to vlastnosť iba na čítanie.

V tomto príklade [K in keyof Person] iteruje cez všetky kľúče rozhrania Person a Person[K] pristupuje k typu každej vlastnosti. Kľúčové slovo readonly robí každú vlastnosť iba na čítanie.

Podmienené typy

Podmienené typy vám umožňujú definovať typy na základe podmienok. Je to výkonný spôsob, ako vytvárať typy, ktoré sa prispôsobujú rôznym scenárom.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // Spracuje null aj undefined
    throw new Error("Hodnota nemôže byť null alebo undefined");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // Výstup: HELLO

  const invalidValue = getValue(null); // Toto vyvolá chybu
  console.log(invalidValue); // Tento riadok sa nedosiahne
} catch (error: any) {
  console.error(error.message); // Výstup: Hodnota nemôže byť null alebo undefined
}

V tomto príklade typ NonNullable<T> kontroluje, či je T null alebo undefined. Ak áno, vráti never, čo znamená, že typ nie je povolený. V opačnom prípade vráti T. To vám umožňuje vytvárať typy, ktoré sú zaručene non-nullable.

Osvedčené postupy pre používanie generík

Tu sú niektoré osvedčené postupy, ktoré treba mať na pamäti pri používaní generík:

Príklady v globálnom kontexte

Pozrime sa na niekoľko príkladov, ako môžu byť generiká použité v globálnom kontexte:

Konverzia mien


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD sa rovná ${amountInEUR} EUR`); // Výstup: 100 USD sa rovná 85 EUR

Formátovanie dátumu


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("Dátum v USA: " + formatDate(currentDate, usDateFormat));
console.log("Nemecký dátum: " + formatDate(currentDate, germanDateFormat));
console.log("Japonský dátum: " + formatDate(currentDate, japaneseDateFormat));

Prekladateľská služba


interface Translation {
  [key: string]: string; // Umožňuje dynamické jazykové kľúče
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const anglickePreklady: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanielskePreklady: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const francuzskePreklady: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const jazykoveData: LanguageData<typeof anglickePreklady>[] = [
  {languageCode: "en", translations: anglickePreklady },
  {languageCode: "es", translations: spanielskePreklady },
  {languageCode: "fr", translations: francuzskePreklady}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `Preklad pre '${key}' v jazyku '${languageCode}' nebol nájdený.`;
  }
  return lang.translations[key] || `Preklad pre '${key}' nebol nájdený.`;
}

console.log(translate("hello", "en", jazykoveData)); // Výstup: Hello
console.log(translate("hello", "es", jazykoveData)); // Výstup: Hola
console.log(translate("welcome", "fr", jazykoveData)); // Výstup: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", jazykoveData)); // Výstup: Preklad pre 'missingKey' v jazyku 'de' nebol nájdený.

Záver

TypeScript generiká sú výkonným nástrojom na písanie opakovane použiteľného, typovo bezpečného kódu, ktorý môže pracovať s komplexnými dátovými typmi. Porozumením základnej syntaxe, pokročilým funkciám a osvedčeným postupom pre generiká môžete výrazne zlepšiť kvalitu a udržateľnosť vašich TypeScript aplikácií. Pri vývoji aplikácií pre globálne publikum vám generiká môžu pomôcť zvládnuť rôznorodé dátové formáty a kultúrne zvyklosti, čím zabezpečíte bezproblémový užívateľský zážitok pre všetkých.